{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "2b573557",
   "metadata": {},
   "source": [
    "# ToolNode 를 사용하여 도구를 호출하는 방법\n",
    "\n",
    "이번 튜토리얼에서는 도구 호출을 위한 LangGraph의 사전 구축된 `pre-built`의 `ToolNode` 사용 방법을 다룹니다.\n",
    "\n",
    "`ToolNode`는 메시지 목록이 포함된 그래프 상태를 입력으로 받아 도구 호출 결과로 상태를 업데이트하는 LangChain Runnable입니다. \n",
    "\n",
    "이는 LangGraph의 사전 구축된 Agent 와 즉시 사용할 수 있도록 설계되었으며, 상태에 적절한 리듀서가 있는 `messages` 키가 포함된 경우 모든 `StateGraph` 와 함께 작동할 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "020ce856",
   "metadata": {},
   "outputs": [],
   "source": [
    "# API 키를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API 키 정보 로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7d1bb130",
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install -qU langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH17-LangGraph-Modules\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bcd34edb",
   "metadata": {},
   "source": [
    "## 도구 정의\n",
    "\n",
    "먼저, 도구를 정의해보겠습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "37f73a88",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.tools import tool\n",
    "from langchain_experimental.tools.python.tool import PythonAstREPLTool\n",
    "from langchain_teddynote.tools import GoogleNews\n",
    "from typing import List, Dict\n",
    "\n",
    "\n",
    "# 도구 생성\n",
    "@tool\n",
    "def search_news(query: str) -> List[Dict[str, str]]:\n",
    "    \"\"\"Search Google News by input keyword\"\"\"\n",
    "    news_tool = GoogleNews()\n",
    "    return news_tool.search_by_keyword(query, k=5)\n",
    "\n",
    "\n",
    "@tool\n",
    "def python_code_interpreter(code: str):\n",
    "    \"\"\"Call to execute python code.\"\"\"\n",
    "    return PythonAstREPLTool().invoke(code)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fd7fa5dc",
   "metadata": {},
   "source": [
    "다음으로는 `ToolNode` 를 사용하여 도구를 호출하는 방법을 살펴보겠습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "22f527a6",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langgraph.prebuilt import ToolNode, tools_condition\n",
    "\n",
    "# 도구 리스트 생성\n",
    "tools = [search_news, python_code_interpreter]\n",
    "\n",
    "# ToolNode 초기화\n",
    "tool_node = ToolNode(tools)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "94a366e9",
   "metadata": {},
   "source": [
    "## `ToolNode`를 수동으로 호출하기"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "98f9c1f4",
   "metadata": {},
   "source": [
    "`ToolNode`는 메시지 목록과 함께 그래프 상태에서 작동합니다. \n",
    "\n",
    "- **중요**: 이때 목록의 마지막 메시지는 `tool_calls` 속성을 포함하는 `AIMessage`여야 합니다.\n",
    "\n",
    "먼저 도구 노드를 수동으로 호출하는 방법을 살펴보겠습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a69ac316",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.messages import AIMessage\n",
    "\n",
    "# 단일 도구 호출을 포함하는 AI 메시지 객체 생성\n",
    "# AIMessage 객체이어야 함\n",
    "message_with_single_tool_call = AIMessage(\n",
    "    content=\"\",\n",
    "    tool_calls=[\n",
    "        {\n",
    "            \"name\": \"search_news\",  # 도구 이름\n",
    "            \"args\": {\"query\": \"AI\"},  # 도구 인자\n",
    "            \"id\": \"tool_call_id\",  # 도구 호출 ID\n",
    "            \"type\": \"tool_call\",  # 도구 호출 유형\n",
    "        }\n",
    "    ],\n",
    ")\n",
    "\n",
    "# 도구 노드를 통한 메시지 처리 및 날씨 정보 요청 실행\n",
    "tool_node.invoke({\"messages\": [message_with_single_tool_call]})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1483c8cb",
   "metadata": {},
   "source": [
    "일반적으로 `AIMessage`를 수동으로 생성할 필요가 없으며, 도구 호출을 지원하는 모든 LangChain 채팅 모델에서 자동으로 생성됩니다.\n",
    "\n",
    "또한 `AIMessage`의 `tool_calls` 매개변수에 여러 도구 호출을 전달하면 `ToolNode`를 사용하여 병렬 도구 호출을 수행할 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "782927c0",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 다중 도구 호출을 포함하는 AI 메시지 객체 생성 및 초기화\n",
    "message_with_multiple_tool_calls = AIMessage(\n",
    "    content=\"\",\n",
    "    tool_calls=[\n",
    "        {\n",
    "            \"name\": \"search_news\",\n",
    "            \"args\": {\"query\": \"AI\"},\n",
    "            \"id\": \"tool_call_id\",\n",
    "            \"type\": \"tool_call\",\n",
    "        },\n",
    "        {\n",
    "            \"name\": \"python_code_interpreter\",\n",
    "            \"args\": {\"code\": \"print(1+2+3+4)\"},\n",
    "            \"id\": \"tool_call_id\",\n",
    "            \"type\": \"tool_call\",\n",
    "        },\n",
    "    ],\n",
    ")\n",
    "\n",
    "# 생성된 메시지를 도구 노드에 전달하여 다중 도구 호출 실행\n",
    "tool_node.invoke({\"messages\": [message_with_multiple_tool_calls]})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2367a18b",
   "metadata": {},
   "source": [
    "## llm 과 함께 사용하기"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d9da378f",
   "metadata": {},
   "source": [
    "도구 호출 기능이 있는 채팅 모델을 사용하기 위해서는 먼저 모델이 사용 가능한 도구들을 인식하도록 해야 합니다. \n",
    "\n",
    "이는 `ChatOpenAI` 모델에서 `.bind_tools` 메서드를 호출하여 수행합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "0af74c1d",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "# LLM 모델 초기화 및 도구 바인딩\n",
    "model_with_tools = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0).bind_tools(tools)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e407303f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 도구 호출 확인\n",
    "model_with_tools.invoke(\"처음 5개의 소수를 출력하는 python code 를 작성해줘\").tool_calls"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7e8fb9c4",
   "metadata": {},
   "source": [
    "보시다시피 채팅 모델이 생성한 AI 메시지에는 이미 `tool_calls`가 채워져 있으므로, 이를 `ToolNode`에 직접 전달할 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f0bc2619",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 도구 노드를 통한 메시지 처리 및 LLM 모델의 도구 기반 응답 생성\n",
    "tool_node.invoke(\n",
    "    {\n",
    "        \"messages\": [\n",
    "            model_with_tools.invoke(\n",
    "                \"처음 5개의 소수를 출력하는 python code 를 작성해줘\"\n",
    "            )\n",
    "        ]\n",
    "    }\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d44807e0",
   "metadata": {},
   "source": [
    "## Agent 와 함께 사용하기"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "45e1dc47",
   "metadata": {},
   "source": [
    "다음으로, LangGraph 그래프 내에서 `ToolNode`를 사용하는 방법을 살펴보겠습니다. \n",
    "\n",
    "Agent 의 그래프 구현을 설정해보겠습니다. 이 **Agent** 는 쿼리를 입력으로 받아, 쿼리를 해결하는 데 필요한 충분한 정보를 얻을 때까지 반복적으로 도구들을 호출합니다. \n",
    "\n",
    "방금 정의한 도구들과 함께 `ToolNode` 및 OpenAI 모델을 사용하게 됩니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "183d54cd",
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangGraph 워크플로우 상태 및 메시지 처리를 위한 타입 임포트\n",
    "from langgraph.graph import StateGraph, MessagesState, START, END\n",
    "\n",
    "\n",
    "# LLM 모델을 사용하여 메시지 처리 및 응답 생성, 도구 호출이 포함된 응답 반환\n",
    "def call_model(state: MessagesState):\n",
    "    messages = state[\"messages\"]\n",
    "    response = model_with_tools.invoke(messages)\n",
    "    return {\"messages\": [response]}\n",
    "\n",
    "\n",
    "# 메시지 상태 기반 워크플로우 그래프 초기화\n",
    "workflow = StateGraph(MessagesState)\n",
    "\n",
    "# 에이전트와 도구 노드 정의 및 워크플로우 그래프에 추가\n",
    "workflow.add_node(\"agent\", call_model)\n",
    "workflow.add_node(\"tools\", tool_node)\n",
    "\n",
    "# 워크플로우 시작점에서 에이전트 노드로 연결\n",
    "workflow.add_edge(START, \"agent\")\n",
    "\n",
    "# 에이전트 노드에서 조건부 분기 설정, 도구 노드 또는 종료 지점으로 연결\n",
    "workflow.add_conditional_edges(\"agent\", tools_condition)\n",
    "\n",
    "# 도구 노드에서 에이전트 노드로 순환 연결\n",
    "workflow.add_edge(\"tools\", \"agent\")\n",
    "\n",
    "# 에이전트 노드에서 종료 지점으로 연결\n",
    "workflow.add_edge(\"agent\", END)\n",
    "\n",
    "\n",
    "# 정의된 워크플로우 그래프 컴파일 및 실행 가능한 애플리케이션 생성\n",
    "app = workflow.compile()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e8674ba2",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_teddynote.graphs import visualize_graph\n",
    "\n",
    "visualize_graph(app)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "603778f5",
   "metadata": {},
   "source": [
    "실행하여 결과를 확인해보겠습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "104486d7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 실행 및 결과 확인\n",
    "for chunk in app.stream(\n",
    "    {\"messages\": [(\"human\", \"처음 5개의 소수를 출력하는 python code 를 작성해줘\")]},\n",
    "    stream_mode=\"values\",\n",
    "):\n",
    "    # 마지막 메시지 출력\n",
    "    chunk[\"messages\"][-1].pretty_print()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "624c4f1a",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 검색 질문 수행\n",
    "for chunk in app.stream(\n",
    "    {\"messages\": [(\"human\", \"search google news about AI\")]},\n",
    "    stream_mode=\"values\",\n",
    "):\n",
    "    chunk[\"messages\"][-1].pretty_print()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "af9323d2",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 도구 호출이 필요 없는 질문 수행\n",
    "for chunk in app.stream(\n",
    "    {\"messages\": [(\"human\", \"안녕? 반가워\")]},\n",
    "    stream_mode=\"values\",\n",
    "):\n",
    "    chunk[\"messages\"][-1].pretty_print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "082f0d6c",
   "metadata": {},
   "source": [
    "`ToolNode`는 도구 실행 중 발생하는 오류도 처리할 수 있습니다. \n",
    "\n",
    "`handle_tool_errors=True`를 설정하여 이 기능을 활성화/비활성화할 수 있습니다(기본적으로 활성화되어 있음)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "langchain-kr-lwwSZlnu-py3.11",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
